package dhadi.model;

import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.json.JSONException;

import dhadi.util.Logger;
import dhadi.util.MoveReader;
import dhadi.util.MoveWriter;
import dhadi.util.Util;

public class Board {

	public static final String PLAYER_A = "Player - A";
	public static final String PLAYER_B = "Player - B";
    private final List<Node> nodes = new ArrayList<Node>();

	private void createNodes() {
		for (int node = (int) 'A'; node <= (int) 'X'; node++) {
			nodes.add(new Node(new String(new byte[] { (byte) node }),this));
		}
	}

	private List<Edge> edges = new ArrayList<Edge>();

	private void createEdges() {
		edges.add(this.createEdge("AB"));
		edges.add(this.createEdge("AJ"));
		edges.add(this.createEdge("BC"));
		edges.add(this.createEdge("BE"));
		edges.add(this.createEdge("CO"));
		edges.add(this.createEdge("DE"));
		edges.add(this.createEdge("DK"));
		edges.add(this.createEdge("EF"));
		edges.add(this.createEdge("EH"));
		edges.add(this.createEdge("FN"));
		edges.add(this.createEdge("GH"));
		edges.add(this.createEdge("GL"));
		edges.add(this.createEdge("HI"));
		edges.add(this.createEdge("IM"));
		edges.add(this.createEdge("JK"));
		edges.add(this.createEdge("JV"));
		edges.add(this.createEdge("KL"));
		edges.add(this.createEdge("KS"));
		edges.add(this.createEdge("LP"));
		edges.add(this.createEdge("MN"));
		edges.add(this.createEdge("MR"));
		edges.add(this.createEdge("NO"));
		edges.add(this.createEdge("NU"));
		edges.add(this.createEdge("OX"));
		edges.add(this.createEdge("PQ"));
		edges.add(this.createEdge("QR"));
		edges.add(this.createEdge("QT"));
		edges.add(this.createEdge("ST"));
		edges.add(this.createEdge("TU"));
		edges.add(this.createEdge("TW"));
		edges.add(this.createEdge("VW"));
		edges.add(this.createEdge("WX"));
	}

	private List<String> triods = new ArrayList<String>();

	private void createTriods() {
		triods.add("ABC");
		triods.add("AJV");
		triods.add("BEH");
		triods.add("COX");

		triods.add("DEF");
		triods.add("DKS");
		triods.add("FNU");
		triods.add("GHI");

		triods.add("GLP");
		triods.add("IMR");
		triods.add("JKL");
		triods.add("MNO");

		triods.add("PQR");
		triods.add("QTW");
		triods.add("STU");
		triods.add("VWX");
	}
	
	private Set<String> cornerNodes = null;
	public Set<String> getCornerNodes() {
	    if (cornerNodes != null) {
	        return cornerNodes;
	    }
	    cornerNodes = new HashSet<String>();
	    cornerNodes.add("A");
	    cornerNodes.add("C");
	    cornerNodes.add("V");
	    cornerNodes.add("X");
	    
	    cornerNodes.add("D");
        cornerNodes.add("F");
        cornerNodes.add("S");
        cornerNodes.add("U");
        
        cornerNodes.add("G");
        cornerNodes.add("I");
        cornerNodes.add("P");
        cornerNodes.add("R");
        return cornerNodes;
	}
	
	private Map<Node, Node> oppositeCornerNodes = null;
	private Map<Node, Node> getOppositeCornerNodes() {
	    if (oppositeCornerNodes != null) return oppositeCornerNodes;
	    
	    oppositeCornerNodes = new HashMap<Node, Node>();
	    oppositeCornerNodes.put(this.findNodeByName("A"), this.findNodeByName("X"));
	    oppositeCornerNodes.put(this.findNodeByName("X"), this.findNodeByName("A"));
	    oppositeCornerNodes.put(this.findNodeByName("C"), this.findNodeByName("V"));
        oppositeCornerNodes.put(this.findNodeByName("V"), this.findNodeByName("C"));
        
        oppositeCornerNodes.put(this.findNodeByName("D"), this.findNodeByName("U"));
        oppositeCornerNodes.put(this.findNodeByName("U"), this.findNodeByName("D"));
        oppositeCornerNodes.put(this.findNodeByName("F"), this.findNodeByName("S"));
        oppositeCornerNodes.put(this.findNodeByName("S"), this.findNodeByName("F"));
        
        oppositeCornerNodes.put(this.findNodeByName("G"), this.findNodeByName("R"));
        oppositeCornerNodes.put(this.findNodeByName("R"), this.findNodeByName("G"));
        oppositeCornerNodes.put(this.findNodeByName("I"), this.findNodeByName("P"));
        oppositeCornerNodes.put(this.findNodeByName("P"), this.findNodeByName("I"));
        
        return oppositeCornerNodes;
	}
	
	/**
	 * Retruns opposite node of the given corner node.
	 * If given node is not corner node then null will be returned.
	 * 
	 * @param node
	 * @return
	 */
	public Node findOppositeCornerNode(Node node) {
	    return this.getOppositeCornerNodes().get(node);	    
	}

	/**
	 * Returns list of triodes on which the Node is a constituent
	 * 
	 * @param node
	 * @return
	 */
	public List<String> findTriods(Node node) {
		List<String> allTriods = this.getTriods();
		List<String> matchingTriods = new ArrayList<String>();
		for (String triode : allTriods) {
			if (triode.indexOf(node.getName()) >= 0) {
				matchingTriods.add(triode);
			}
		}
		return matchingTriods;
	}

	private Deque<Move> moves;
	private Player playerA;
	private Player playerB;
	private Player currentPlayer;
	private MoveWriter moveWriter;
	private Pick lastMovedPick;

	private Board(Player a, Player b) {
		this.initializeBoard();
		this.playerA = a;
		this.playerA.initializePlayer(this);
		
		this.playerB = b;
		this.playerB.initializePlayer(this);

		this.playerA.setOpponent(this.playerB);
		this.playerB.setOpponent(this.playerA);
		this.moves = new ArrayDeque<Move>();
		this.currentPlayer = this.playerA;
		try {
			this.moveWriter = Util.createMoveWriter();
		} catch (IOException e) {
			Logger.log(Board.class, "While creating move writer", e);
		}
	}
	
	public static Board createBoard(Player a, Player b)
	{
	    return new Board(a, b);
	}

	private void initializeBoard() {
		this.createNodes();
		this.createEdges();
		this.createTriods();
		this.computeEdgesOnNodes();
	}

	private void computeEdgesOnNodes() {
		for (Node n : this.nodes) {
			n.setEdges(this.findTouchingEdges(n));
		}
	}
	
	/**
	 * Returns movable picks on board for the give
	 * player.
	 * 
	 * @param player
	 * @return List<Pick>
	 */
	public List<Pick> findMovablePicks(Player player) {
	    List<Pick> onBoardPicks = player.getPicksOnBoard();
	    List<Pick> movablePicks = new ArrayList<Pick>();
	    if (onBoardPicks.isEmpty()) return movablePicks;
	    
	    for (Pick onBoardPick : onBoardPicks) {
	        if (!onBoardPick.getNode().getAdjusantEmptyNodes().isEmpty())
	            movablePicks.add(onBoardPick);
	    }
	    return movablePicks;
	}
	
	/**
	 * Returns List of player picks which can be moved
	 * to node
	 * 
	 * @param opponent
	 * @param node
	 * @return
	 */
    public List<Pick> findMovablePicks(Player player, Node node) {
        if (player == null || node == null) return Collections.emptyList();
        List<Pick> movablePicks = new ArrayList<Pick>();
        List<Node> adjacentNodes = node.getAdjusantNodes();
        for (Node adjacentNode : adjacentNodes) {
            if (!adjacentNode.isNodeEmpty() && 
                    adjacentNode.pick.getOwner().equals(player)) {
                movablePicks.add(adjacentNode.pick);
            }
        }
        return movablePicks;
    }
    
    /**
     * Returns List of player picks which can be moved
     * only to node and there is no other node that this
     * pick can move to.
     * 
     * @param opponent
     * @param movableNode
     * @return
     */
    public List<Pick> findPicksOnlyMovableToNode(Player player,
            Node node) {
        if (player == null || node == null) return Collections.emptyList();
        List<Pick> onlyMovablePicks = new ArrayList<Pick>();
        
        List<Pick> movablePicks = this.findMovablePicks(player,node);
        for (Pick movablePick : movablePicks) {
            if (this.findMovableNodes(movablePick).size() == 1) {
                onlyMovablePicks.add(movablePick);
            }
        }
        return onlyMovablePicks;
    }
	
	/**
	 * Returns list of Nodes to which a given onBoard pick can
	 * be moved
	 * 
	 * @param pick
	 * @return
	 */
	public List<Node> findMovableNodes(Pick pick) {
	    if (!pick.isOnboard()) return Collections.emptyList();
	    return pick.getNode().getAdjusantEmptyNodes();	    	    	    
	}

	private List<Edge> findTouchingEdges(Node n) {
		List<Edge> touchingEdges = new ArrayList<Edge>();
		for (Edge e : this.edges) {
			if (e.doesTouches(n))
				touchingEdges.add(e);
		}
		return touchingEdges;
	}
	
	public List<String> findTouchingTriodes(Node n) {
		List<String> touchingTriodes = new ArrayList<String>();
		List<String> triodes = this.getTriods();
		for (String triode : triodes) {
			if (triode.indexOf(n.name) != -1) {
				touchingTriodes.add(triode);
			}
		}
		return touchingTriodes;
	}
	
	public List<String> findTouchingTriodes(List<Pick> picks) {
		List<String> touchingTriodes = new ArrayList<String>();
		if (picks == null || picks.isEmpty()) return touchingTriodes;
		for (Pick pick : picks) {
			touchingTriodes.addAll(this.findTouchingTriodes(pick.getNode()));
		}
		return touchingTriodes;
	}
	
	public List<String> findTouchingTriodes(String triode) {
		List<String> touchingTriodes = new ArrayList<String>();
		if (triode == null) return touchingTriodes;
		Node node;
		for (int i=0; i<triode.length(); i++) {
			node = this.findNodeByName(""+triode.charAt(i));
			List<String> nodeTriodes = this.findTouchingTriodes(node);
			for (int n = nodeTriodes.size()-1; n >= 0; n--) {
				if (nodeTriodes.get(n).equalsIgnoreCase(triode)) {
					nodeTriodes.remove(n);
				}
			}
			touchingTriodes.addAll(nodeTriodes);
		}	
		return touchingTriodes;
	}
	
	/**
	 * Returns intersecting Node(s) of the given triodes
	 * @param triodeA
	 * @param triodeB
	 * @return
	 */
	public Node findTriodeIntersection(String triodeA, String triodeB) {
		if (triodeA == null || triodeB == null) return null;
		for (int i=0; i<triodeA.length(); i++) {
			if (triodeB.indexOf(triodeA.charAt(i)) > -1) {
				return this.findNodeByName(""+triodeA.charAt(i));
			}
		}
		return null;
	}

	public List<Node> getNodes() {
		return nodes;
	}

	public List<Edge> getEdges() {
		return edges;
	}

	public List<String> getTriods() {
		return triods;
	}

	public List<Pick> getPlayerAPics() {
		return this.playerA.getPicks();
	}

	public List<Pick> getPlayerBPics() {
		return this.playerB.getPicks();
	}
	
	public Player getPlayerA() {
		return this.playerA;
	}

	public Player getPlayerB() {
		return this.playerB;
	}

	public List<Node> getEmptyNodes() {
		List<Node> nodes = this.getNodes();
		List<Node> emptyNodes = new ArrayList<Node>(nodes.size());
		for (Node n : nodes) {
			if (n.isNodeEmpty())
				emptyNodes.add(n);
		}
		return emptyNodes;
	}
	
	public List<Node> getEmptyNodes(String triode) {
		List<Node> emptyNodes = new ArrayList<Node>();
		if (triode == null) return emptyNodes;
		
		Node node;
		for (int i=0; i<triode.length(); i++) {
			node = this.findNodeByName(""+triode.charAt(i));
			if (node.isNodeEmpty()) {
				emptyNodes.add(node);
			}
		}
		return emptyNodes;
	}
	
	public List<Node> getTriodeNodes(String triode) {
		List<Node> triodeNodes = new ArrayList<Node>();
		if (triode == null) return triodeNodes;
		
		Node node;
		for (int i=0; i<triode.length(); i++) {
			node = this.findNodeByName(""+triode.charAt(i));
			triodeNodes.add(node);
		}
		return triodeNodes;
	}
	
	public List<Pick> getPlayerPicksOnNodes(List<Node> nodes, Player player) {
		List<Pick> playerPicks = new ArrayList<Pick>();
		if (nodes == null || nodes.isEmpty()) return playerPicks;
		if (player == null) return playerPicks;
		for (Node node : nodes) {
			if (node.pick == null) continue;
			if (node.pick.getOwner().equals(player)) playerPicks.add(node.pick);
		}
		return playerPicks;
	}
	
	public List<Pick> getPlayerPicksOnTriode(String triode, Player player) {
        return this.getPlayerPicksOnNodes(this.getTriodeNodes(triode), player);
    }
	
	/**
	 * Returns true if given triode is occupied
	 * only by the player's opponent.
	 * 
	 * @param triode
	 * @param player
	 * @return
	 */
	public boolean isOnlyOpponentOccupied(String triode, Player player) {
		List<Node> triodeNodes = this.getTriodeNodes(triode);
		List<Pick> opponentPicks = this.getPlayerPicksOnNodes(
				triodeNodes, player.getOpponent());
		List<Pick> myPicks = this.getPlayerPicksOnNodes(triodeNodes, player);
		return myPicks.isEmpty() && !opponentPicks.isEmpty();
	}
	
	/**
	 * If all nodes are empty on given triode
	 * returns true otherwise false.
	 * 
	 * @param triode
	 * @return
	 */
	public boolean isTriodeEmpty(String triode) {
		List<Node> triodeNodes = this.getTriodeNodes(triode);
		for (Node triodeNode : triodeNodes) {
			if (!triodeNode.isNodeEmpty()) return false;			
		}
		return true;
	}
	
	private Edge createEdge(String string) {
		return new Edge(this.findNodeByName(string.substring(0, 1)), this
				.findNodeByName(string.substring(1, 2)));
	}

	public Node findNodeByName(String nodeName) {
		for (Node n : this.nodes) {
			if (n.name.equalsIgnoreCase(nodeName))
				return n;
		}
		return null;
	}
	
	public Pick findPickByName(String pickName) {
	    Pick pick = this.findPick(pickName,this.playerA.getPicks());
	    if (pick != null) return pick;
	    return this.findPick(pickName,this.playerB.getPicks());
	}

	private Pick findPick(String pickName, List<Pick> picks) {
        if (Util.isEmpty(pickName) || picks == null || picks.isEmpty()) return null;
        for (Pick pick : picks) {
            if (pick.getName().equals(pickName)) return pick;
        }
        return null;
    }

    public Player currentPlayer() {
		return this.currentPlayer;
	}

	/*
	 * public void togglePlayer() { if (this.currentPlayer.equals(this.playerA))
	 * this.currentPlayer = this.playerB; else if
	 * (this.currentPlayer.equals(this.playerB)) this.currentPlayer =
	 * this.playerA; }
	 */

	public void switchMover() {
		if (this.currentPlayer.equals(this.playerA)) {
			this.currentPlayer = this.playerB;
			this.currentPlayer.makeAMove();
			this.playerA.onlyView();
		} else {
			this.currentPlayer = this.playerA;
			this.currentPlayer.makeAMove();
			this.playerB.onlyView();
		}
	}

	public synchronized void move(Move move) {

		Logger.log(this.getClass(), move.toString());

		// Validate Move.
		if (!this.validateMove(move.getPlayer(), move)) {
			return;
		}
		
		this.updateModel(move);
		
		// Validate Post Move
		if (!this.validatePostMove(move.getPlayer(), move)) {
			this.rollbackModel();
			
			// Notify current user to move again.
			// 
			this.currentPlayer.getOpponent().onlyView();
			this.currentPlayer.makeAMove();
			return;
		}
		
		if (this.moveWriter != null) {
			this.moveWriter.write(move);
		}
		
		// Mark last moved pick
		this.setLastMovedPick(move.getPick());
		
		Player winner = this.decideWinner();
		if (winner != null) {
			winner.youTheWinner();
			winner.getOpponent().youTheLooser();
			winner.setMessage(winner.getName() + " wins. Game Over");
			return;
		}

		if (this.isDhadiMove(move.getPlayer(), move)) {
			move.getPlayer().dhadiByYou();
			move.getPlayer().getOpponent().dhadiOnYou();
		} else {
			move.getPlayer().resetDhadiFlags();
			move.getPlayer().getOpponent().resetDhadiFlags();
			this.switchMover();
		}
	}

	private void setLastMovedPick(Pick pick) {
		if (this.lastMovedPick != null) {
			this.lastMovedPick.setLastMovedFlag(false);
		}
		this.lastMovedPick = pick;
		this.lastMovedPick.setLastMovedFlag(true);
	}

	// We dont need this method, we can
	// model a kill using 'Move'
	/*
	 * public void kill(Pick pick) {
	 * 
	 * }
	 */

	/* ============ GameLogic ============================== */

	public boolean isPlayersTurn(Player player) {
		return this.currentPlayer().equals(player);
	}

	// Not needed, this should be part of UI
	/*
	 * public void detectMove(Player player) { throw new
	 * RuntimeException("Not implemented"); }
	 */

	/**
	 * Rules: 1. Player cannot move a pick on board until all the Unused picks
	 * exists.
	 * 
	 * 2. Player cannot move a pick on board more than one edge at a time.
	 * 
	 * 3. Pick cannot be moved from and to the same node
	 */
	public boolean validateMove(Player player, Move move) {
		
	    String message;
		if (this.isGameOver()) {
		    message = "Game Over";
		    Logger.log(Board.class, message);
			move.getPlayer().setMessage(message);
			return false;
		}
		
		// Check whether current players move
		if (!this.isPlayersTurn(move.getPlayer())) {
		    message = "Wait for your opponent to make a move";
            Logger.log(Board.class, message);
			move.getPlayer().setMessage(message);
			return false;
		}
		
		// Player cannot move picks until all his unused picks are
		// completely used, unless he/she is making a kill.
		if (player.getUnUsedPicks().size() > 0 && 
			!player.getUnUsedPicks().contains(move.getPick()) &&
			!player.isDhadiByYou()) {
		    message = "Move not allowed";
            Logger.log(Board.class, message);
			move.getPlayer().setMessage(message);
			return false;
		}

		// Pick cannot be moved from and to the same node
		if (move.whatMove() == Move.PICK_MOVED_ON_BOARD
				&& move.getToNode().equals(move.getFromNode())) {
		    message = "Move again";
            Logger.log(Board.class, message);
			move.getPlayer().setMessage(message);
			return false;
		}
		
		return true;
	}
	
	/**
	 * This method should be used to 
	 * query the Game state. The state of
	 * game winner is derived in decideWinner() method.
	 * 
	 * Game over if
	 * 1. Any player is a winner.
	 * 
	 * 
	 * @return
	 */
	public boolean isGameOver() {
		
		// 1. if a player wins, game is over
		return this.playerA.areYouWinner() || this.playerB.areYouWinner(); 
	}

	/**
	 * Validates after the move is made
	 * 1. If opponent could not move after this move, ask
	 *    current player to make alternate move.
	 *    
	 * @param player
	 * @param move
	 * @return
	 */
	public boolean validatePostMove(Player player, Move move) {
		
		if (!player.getOpponent().canMove()) {
			move.getPlayer().setMessage("Your opponent cannot move, please take an alternate move");
			return false;
		}
		
		if (this.isDhadiMove(move.getPlayer(), move) && 
			!move.getPlayer().canKill()) {
			move.getPlayer().setMessage("You cannot kill any pick, please take an alternat move");
			return false;
		}
		
		return true;
	}

	/**
	 * Returns true only if the move is a dhadi move
	 * 
	 * @param player
	 * @param move
	 * @return
	 */
	public boolean isDhadiMove(Player player, Move move) {
		Node node = move.getToNode();
		if (node == null) return false; // probably the move is a kill
		if (player.isPickFormaingDhadi(move.getPick()))
			return true;
		else
			return false;
	}

	/**
	 * Following are the move data for each type of move
	 * 
	 * PICK_PLACED_ON_BOARD, PICK_MOVED_ON_BOARD move.player is the player who
	 * made this move move.pick is the pick that is moved, whose status will be
	 * Pick.USED move.toNode will be non null value move.fromNode can be null if
	 * PICK_PLACED_ON_BOARD
	 * 
	 * PICK_KILLED move.player is the player who made this move move.pick is the
	 * pick that is moved, whose status will be Pick.DESTROYED move.toNode will
	 * be null move.fromNode will be a valid node
	 * 
	 * @param move
	 */
	private void updateModel(Move move) {
		this.moves.push(move);
		switch (move.whatMove()) {
		case Move.PICK_PLACED_ON_BOARD:
		case Move.PICK_MOVED_ON_BOARD:
			move.getPick().setNode(move.getToNode());
			break;

		case Move.PICK_KILLED:
			move.getPick().destroy();
			break;

		default:
			break;
		}
	}
	
	private void rollbackModel() {
		if (this.moves.size() == 0) return;
		Move move = this.moves.pop();
		switch (move.whatMove()) {
		case Move.PICK_PLACED_ON_BOARD:
			move.getPick().releaseNode();
			move.getPick().setState(Pick.STATE.UNUSED);
			break;
			
		case Move.PICK_MOVED_ON_BOARD:
			move.getPick().releaseNode();
			move.getPick().setNode(move.getFromNode());
			break;
		
		case Move.PICK_KILLED:
			move.getPick().setNode(move.getFromNode());
			break;
			
		default: break;
			
		}
		
		/*Player oldCurrentPlayer = this.currentPlayer;
		
		this.currentPlayer = move.getPlayer();*/		
	}

	/**
	 * This method is not implemented,
	 * use findMovableNodes instead
	 * @param pick
	 * @return
	 */
	public Node[] possibleNodesToMove(Pick pick) {
		throw new RuntimeException("Not implemented");
	}	

	public Player decideWinner() {
		// Of 11 picks player should have at least 3 picks
		// to continue game
		if (this.playerA.getPicksLost().size() > 8)
			return this.playerB;

		if (this.playerB.getPicksLost().size() > 8)
			return this.playerA;

		// No winner at this time
		return null;
	}

    public void loadMoves(String movesFile) throws IOException {
        MoveReader mReader = Util.createMoveReader(movesFile);
        SerializationContext context = 
            SerializationContext.createForLoadingMoves(this);
        
        List<Move> moves = null;
        try {
            moves = mReader.getMoves(context);
        } catch (JSONException e) {
            Logger.log(Board.class, 
                    "Exceptin while loading moves from file: "+movesFile,
                    e);
        }
        for (Move move : moves) {
            this.move(move);            
        }
    }

    /**
     * Returns PlayerA if the given player is equal to PlayerA,
     * PlayerB if the given player is equal to PlayerB,
     * null otherwise.
     * 
     * @param fromJson
     * @return Player
     */
    public Player getMatchinPlayer(Player fromJson) {
        if (fromJson == null) return null;
        if (this.playerA.equals(fromJson)) return this.playerA;
        if (this.playerB.equals(fromJson)) return this.playerB;
        return null;
    }    	
}
